package com.spring.sso.config.security.adapter;

import com.spring.sso.config.common.AuthenticationConfig;
import com.spring.sso.config.common.Constant;
import com.spring.sso.config.security.filter.oauth2.OAuth2PasswordAuthFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : pangfuzhong
 * @description
 * @date : 2021/9/27 11:21
 *
 *      spring security filter: 过滤器顺序
 *
 *          ApplicationFilterChain
 *
 *          FilterChainProxy.VirtualFilterChain <-- spring security过滤器链
 *
 *              filters:
 *                  WebAsyncManagerIntegrationFilter
 *                  SecurityContextPersistenceFilter
 *                  HeaderWriterFilter
 *                  LogoutFilter
 *                  OAuth2AuthorizationRequestRedirectFilter  <-- @EnableAuthorizationServer该注解配置会增加此过滤器，详见源码
 *                  OAuth2LoginAuthorizationFilter
 *                  RequestCacheAwareFilter
 *                  SecurityContextHolderAwareRequestFilter
 *                  AnonymousAuthenticationFilter
 *                  SessionManagementFilter
 *                  ... <-- 自定义过滤器加入地方 比如当前测试OAuth2使用 OAuth2PasswordAuthFilter, 可以配置再此
 *                  ExceptionTranslationFilter
 *                  FilterSecurityInterceptor <-- 该过滤器会触发HttpServlet执行controller接口的调用
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ServerWebSecurityConfigureAdapter extends WebSecurityConfigurerAdapter {
    public static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(31);
    private ApplicationContext applicationContext;

    @Autowired
    private AuthenticationConfig authenticationConfig;

    @Override
    @Autowired
    public void setApplicationContext(ApplicationContext context) {
        this.applicationContext = context;
        super.setApplicationContext(context);
        try {
            AuthenticationManager authenticationManager = super.authenticationManagerBean();
            AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration = context.getBean(AuthorizationServerEndpointsConfiguration.class);
            authorizationServerEndpointsConfiguration.getEndpointsConfigurer().authenticationManager(authenticationManager);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置登录认证过程用户服务类
        auth.userDetailsService(this.authenticationConfig.getUserDetailsService()).passwordEncoder(encoder);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略options预请求, spring security将会忽略options类型的浏览器发送的预请求
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    /**
     * note1: 在spring security中配置filter一定要注意顺序, 否则会出现已登陆用户信息被洗掉的情况，比如登陆filter排在SecurityContextPersistenceFilter前面会出现白登陆
     * note2: oauth2验证流程，已开放平台来看
     *      1、关于第三方登陆认证流程(以B站和QQ两个三方平台来看,即B站需要共享用户在QQ平台的信息);
     *          2.1、登陆B站应用login.html页面，页面处选择QQ图标，会向B站后台服务请求QQ登陆，最后B站后台返回, 该返回为重定向返回
     *              --> B站后台拦截请求，该拦截被过滤器OAuth2AuthorizationRequestRedirectFilter进行拦截后台，由于B站后台配置了自身应用在QQ开放品台注册的appId、app_secret等, 那么该过滤器会重定向返回，返回地址为：
     *              https://graph.qq.com/oauth2.0/authorize?
     *                  response_type=code  &
     *                  state=authorize     &
     *                  client_id=101135748 &
     *                  redirect_uri=https%3A%2F%2Fpassport.bilibili.com%2Flogin%2Fsnsback%3Fsns%3Dqq%26%26state%3D9ef154702c0411ecab1ee2a92205464f&
     *                  scope=do_like,get_user_info,get_simple_userinfo,get_vip_info,get_vip_rich_info,add_one_blog,list_album,upload_pic,add_album,list_photo,get_info,add_t,del_t,add_pic_t,get_repost_list,get_other_info,get_fanslist,get_idollist,add_idol,del_idol,get_tenpay_addr
     *
     *              --> 由于重定向到了 [https://graph.qq.com/oauth2.0/authorize?] 该地址，那么QQ后台会处理该请求，由于没有登录过QQ，那qq直接进行授权登陆页重定向返回，最终浏览器看到的是qq返回的授权登陆页
     *                  即重定向到https://graph.qq.com/oauth2.0/show?
     *
     *         2.2、用户使用手机微信app进行扫码，微信扫码后
     *
     *      2、hai家校接入单点登陆并基于oauth2进行登陆认证授权
     *          2.1、hai家校应用在开放平台进行应用注册, 生成client_id和client_secret以及配置接口待访问的接口权限(可选)
     *              2.1.1、仅对接登陆, 不访问任何开放平台对外接口;
     *                  --> 入驻到Saas平台以内嵌应用访问, 那么只需登陆Saas系统即可, 再点击hai家校应用图标时，跨域请求到hai家校页面, 将token待过去，hai家校中的任何后台业务接口请求时向saas网关发起token校验请求
     *                      该请求验证token接口对外开放
     *                  --> 不入驻saas平台，单独的系统，使用同一账号实现免登陆, 此处需要前端进行处理，hai家校页面在访问后台接口前，前端判断是否存在单点登陆接口，没有则向前端sso站点跳转，获取token，依旧没有则返回登陆页
     *              2.1.2、对接单点登陆及数据接口
     *                  --> 入驻到Saas平台以内嵌应用访问:
     *                  --> 不入驻saas平台，单独的系统，使用同一账号实现免登陆, 此处需要前端进行处理，hai家校页面在访问后台接口前，前端判断是否存在单点登陆接口，没有则向前端sso站点跳转，获取token，依旧没有则返回登陆页
     *
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 允许跨域访问
        //http.cors();
        // 关闭跨站请求伪造(如果使用了基于token的认证，此时可无需开启csrf)
        //http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        // 关闭session(前后端分离无需使用到session) --> 禁止session使用导致oauth2通过请求request所属线程的ThreadLocal会获取不到登陆用户principal
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

        // --> 配置oauth2 认证登陆页测试，如果不是用登陆页进行其他登陆后测试, 需要将该配置注释掉
        http.oauth2Login().loginPage("/authLogin").permitAll();

        // --> 认证过滤条件
        http.authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS, Constant.MATCH_ALL_PATH).permitAll()
            .antMatchers(HttpMethod.POST, /*this.authenticationConfig.getOpenLoginAuthUrl(), */this.authenticationConfig.getLoginAuthUrl(), this.authenticationConfig.getTokenAuthUrl(), this.authenticationConfig.getVerificationAuthUrl(), this.authenticationConfig.getWhiteListUrl()).permitAll()
            .antMatchers("/oauth/*").permitAll()
            .anyRequest().authenticated(); // 没有定义的请求，所有的角色都可以访问（tmp也可以）

        // 1、oauth2登陆过滤器
        ProviderManager providerManager = (ProviderManager) authenticationManager();
        providerManager.getProviders().stream()
            .filter(authenticationProvider -> authenticationProvider instanceof DaoAuthenticationProvider)
            .forEach(authenticationProvider -> {
                ((DaoAuthenticationProvider) authenticationProvider).setHideUserNotFoundExceptions(Boolean.FALSE);
            });
        OAuth2PasswordAuthFilter oAuth2PasswordAuthFilter = new OAuth2PasswordAuthFilter(this.authenticationConfig);
        oAuth2PasswordAuthFilter.setUsernameParameter(Constant.USERNAME);
        oAuth2PasswordAuthFilter.setPasswordParameter(Constant.PASSWORD);
        oAuth2PasswordAuthFilter.setAuthenticationManager(providerManager);
        // 配置用户身份认证成功后是否继续过滤器链的执行
        //oAuth2PasswordAuthFilter.setContinueChainBeforeSuccessfulAuthentication(Boolean.TRUE);
        http.addFilterAfter(oAuth2PasswordAuthFilter, SessionManagementFilter.class);

        //开启记住我功能,session和cookie实现，默认两周时间
        //http.rememberMe().rememberMeParameter("rememberme");
    }

    //@Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 携带服务器端验证后允许的跨域请求域名, 即 Access-Control-Allow-Origin 参数, 即本服务允许该参数指定的域名进行跨域访问
        configuration.addAllowedOrigin("*");
        // 服务端告知浏览器当前端配置了withCredentials属性为true时，在跨域访问时带上认证信息(cookie) 即 Access-Control-Allow-Credentials
        configuration.setAllowCredentials(true);
        // 服务端告知浏览器可以在实际发送跨域请求时，支持的请求类型, 即 Access-Control-Allow-Headers
        configuration.addAllowedHeader(CorsConfiguration.ALL);
        // 服务端告知浏览器可以在实际发送跨域请求时，跨域请求支持的方法, 即 Access-Control-Allow-Methods
        configuration.addAllowedMethod(CorsConfiguration.ALL);
        // 当不同源的的网站所在浏览器发起的请求都按照此跨域配置进行
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
